package org.dicr.sys.linux;

import java.io.*;
import java.lang.ref.*;
import java.util.*;
import java.util.logging.*;

import org.dicr.mng.exc.*;
import org.dicr.sys.*;
import org.dicr.util.data.*;
import org.dicr.util.data.exc.*;
import org.dicr.util.net.*;

/**
 * Файл привязки MAC к IP.
 * <P>
 * Данный файл служит для статической привязки MAC к IP-адресам. Используется
 * командой <CODE>/sbin/arp -f</CODE>, имеет стандартное имя
 * <CODE>/etc/ethers</CODE>.
 * </P>
 * <P>
 * Формат файла:
 * 
 * <PRE>
 * &lt;I&gt;192.168.0.2&lt;/I&gt; &lt;I&gt;00:01:02:03:04:05&lt;/I&gt; &lt;I&gt;192.168.0.3&lt;/I&gt; &lt;I&gt;00:01:02:03:04:07&lt;/I&gt;
 * </PRE>
 * 
 * </P>
 * <P>
 * Для разделения доступа, класс содержит реестр создаваемых обьектов. Если
 * обьект, связанный с файлом уже существует, то возвращается экземпляр, с
 * <CODE>synchronized</CODE> методами доступа, иначе создается новый.
 * </P>
 * 
 * @author Igor A Tarasov, &lt;java@dicr.org&gt;
 * @version 1.0
 */
public class EtherFile extends SystemFileHandler {

	/**
	 * Логгер.
	 */
	private static final Logger log = Logger.getLogger(EtherFile.class.getName());

	/**
	 * Default file
	 */
	public static final String DEFAULT_FILE = "/etc/ethers";

	/**
	 * Команда <CODE>arp</CODE>
	 */
	public static final String CMD_ARP = "/sbin/arp";

	/**
	 * Время модификации файла.
	 */
	protected long fileTime = 0;

	/**
	 * Кэш адресов.
	 */
	protected SoftReference<Map<IP, MAC>> hostsRef = null;

	/**
	 * Constructor
	 * 
	 * @param fileName
	 * @throws FileLockedException
	 */
	protected EtherFile(String fileName) throws FileLockedException {
		super(fileName);
		this.fileTime = 0;
		this.hostsRef = null;
	}

	/**
	 * Return instance for given name
	 * 
	 * @param fileName
	 *            file name
	 * @return instance for file name
	 * @throws FileLockedException
	 *             file already locked
	 */
	public static synchronized EtherFile getInstance(String fileName) throws FileLockedException {
		if (fileName == null) {
			throw new IllegalArgumentException("null file name");
		}
		String absName = new File(fileName).getAbsolutePath();
		EtherFile etherFile = null;
		SystemFileHandler handler = getHandler(absName);
		if (handler != null) {
			if (handler instanceof EtherFile) {
				etherFile = (EtherFile) handler;
			} else {
				throw new FileLockedException(absName);
			}
		} else {
			etherFile = new EtherFile(absName);
		}
		return etherFile;
	}

	/**
	 * Возвращает экземпляр обьекта, связанный с {@link #DEFAULT_FILE} файлом.
	 * 
	 * @return экземпляр обьекта.
	 * @throws FileLockedException
	 *             if file already locked by other config implementation class
	 */
	public static EtherFile getInstance() throws FileLockedException {
		return getInstance(DEFAULT_FILE);
	}

	/**
	 * Возвращает данные файла.
	 * <P>
	 * Данные кэшируются. Устаревание данных определяется по времени модификации
	 * файла.
	 * </P>
	 * 
	 * @throws IOException
	 *             ошибка чтения файла.
	 * @return данные файла хостов.
	 */
	protected synchronized Map<IP, MAC> getFileData() throws IOException {
		Map<IP, MAC> hosts = null;
		if (this.hostsRef != null) {
			hosts = this.hostsRef.get();
		}
		if (hosts != null && this.fileTime != 0 && new File(getFileName()).lastModified() <= this.fileTime) {
			return hosts;
		}
		this.hostsRef = null;
		this.fileTime = 0;
		hosts = new HashMap<>();
		BufferedReader in = null;
		try {
			in = new BufferedReader(new FileReader(getFileName()));
			for (int ln = 1;; ln++) {
				try {
					String line = in.readLine();
					if (line == null) {
						break;
					}
					line = StringUtils.normalize(line);
					if (line == null) {
						continue;
					}
					if (line.startsWith("#") || line.startsWith("//")) {
						continue;
					}
					String[] fields = line.split("\\s+");
					if (fields.length < 2) {
						throw new FileFormatException(getFileName(), ln);
					}
					IP ip = new IP(fields[0]);
					MAC mac = new MAC(fields[1]);
					hosts.put(ip, mac);
				} catch (IncorrectAddressException ex) {
					log.log(Level.WARNING,
							String.format("error in line %d of file %s", Integer.valueOf(ln), getFileName()), ex);
				} catch (FileFormatException ex) {
					log.log(Level.WARNING,
							String.format("error in line %d of file %s", Integer.valueOf(ln), getFileName()), ex);
				}
			}
			in.close();
			in = null;
			this.hostsRef = new SoftReference<>(hosts);
			this.fileTime = new File(getFileName()).lastModified();
		} catch (FileNotFoundException ex) {
			log.log(Level.WARNING, "ethers file not exists", ex);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (Exception ex) {
					// NOP
				}
			}
		}
		return hosts;
	}

	/**
	 * Сохраняет файл и фиксирует адреса. <BLOCKQUOTE>
	 * <CODE>arp -f fileName</CODE></BLOCKQUOTE>
	 * <P>
	 * Сохраняемые адреса помещаются в кэш.
	 * </P>
	 * 
	 * @param hosts
	 *            данные для сохранения.
	 * @throws IOException
	 *             ошибка сохранения файла.
	 */
	protected synchronized void saveFileData(Map<IP, MAC> hosts) throws IOException {
		assert (hosts != null);
		PrintWriter ou = null;
		try {
			// create temp file
			File tmpFile = new File(getFileName() + ".tmp." + System.currentTimeMillis());
			ou = new PrintWriter(new BufferedWriter(new FileWriter(tmpFile)));
			// save file
			Set<IP> ips = hosts.keySet();
			for (IP ip : ips) {
				ou.printf("%s\t%s\n", ip, hosts.get(ip));
			}
			ou.flush();
			ou.close();
			// rename temp file
			if (!tmpFile.renameTo(new File(getFileName()))) {
				throw new IOException(String.format("error renaming file '%s' to '%s'", tmpFile.getAbsolutePath(),
						getFileName()));
			}
			this.fileTime = new File(getFileName()).lastModified();
			this.hostsRef = new SoftReference<>(hosts);
		} finally {
			if (ou != null) {
				try {
					ou.close();
				} catch (Exception ex) {
					// NOP
				}
			}
		}
	}

	/**
	 * Преобразует команду в строку.
	 * 
	 * @param command
	 *            команда.
	 * @return строка команды.
	 */
	protected static String commandToString(List<String> command) {
		assert (command != null);
		StringBuilder sb = new StringBuilder();
		for (String cp : command) {
			sb.append(cp);
			sb.append(' ');
		}
		return sb.toString();
	}

	/**
	 * Выполняет команду.
	 * 
	 * @param command
	 *            команда.
	 * @throws CommandException
	 *             ошибка выполнения.
	 */
	protected static synchronized void runCommand(List<String> command) throws CommandException {
		assert (command != null);
		log.finest("running command: " + commandToString(command));
		InputStream in = null;
		try {
			ProcessBuilder pb = new ProcessBuilder(command);
			pb.redirectErrorStream(true);
			Process p = pb.start();
			in = new BufferedInputStream(p.getInputStream());
			while (in.read() != -1) {
				// NOP
			}
			in.close();
			in = null;
		} catch (IOException ex) {
			throw new CommandException(command, ex);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (Exception ex) {
					// NOP
				}
			}
		}
	}

	/**
	 * Возвращает адреса.
	 * 
	 * @return список адресов.
	 * @throws IOException
	 *             ошибка чтения файла.
	 */
	public synchronized HashMap<IP, MAC> list() throws IOException {
		return new HashMap<>(getFileData());
	}

	/**
	 * Устанавливает MAC адрес.
	 * 
	 * @param ip
	 *            числовое значение IP-адреса.
	 * @param mac
	 *            числовое значние MAC-адреса.
	 * @throws IOException
	 *             ошибка чтения/записи файла.
	 * @throws CommandException
	 *             ошибка выполнения команды arp.
	 */
	public synchronized void set(IP ip, MAC mac) throws IOException, CommandException {
		if (ip == null) {
			throw new IllegalArgumentException("null ip");
		}
		if (mac == null) {
			throw new IllegalArgumentException("null mac");
		}
		Map<IP, MAC> hosts = getFileData();
		hosts.put(ip, mac);
		saveFileData(hosts);
		log.finer(String.format("updated ether '%s' : '%s'", ip, mac));
		// run arp fix command
		List<String> command = new ArrayList<>();
		command.add(CMD_ARP);
		command.add("-s");
		command.add(ip.toString());
		command.add(mac.toString());
		runCommand(command);
	}

	/**
	 * Удаляет MAC-адрес.
	 * 
	 * @param ip
	 *            IP-адрес.
	 * @throws IOException
	 *             ошибка чтения/заприси файла.
	 * @throws CommandException
	 *             ошибка выполнения команды arp.
	 */
	public synchronized void reset(IP ip) throws IOException, CommandException {
		Map<IP, MAC> hosts = getFileData();
		hosts.remove(ip);
		saveFileData(hosts);
		log.finer(String.format("removed ether '%s'", ip));
		// run arp fix command
		List<String> command = new ArrayList<>();
		command.add(CMD_ARP);
		command.add("-d");
		command.add(ip.toString());
		runCommand(command);
	}
}
